﻿using System;
using System.Linq;
using System.Linq.Expressions;

namespace Ebt.Linq
{
	/// <summary>
	/// Modifies expression tree so it can be applied to EF data context object set.
	/// </summary>
	/// <remarks>
	/// The following transformations are made:
	/// 
	/// 1. Change root queryable object to ObjectSet instance. Initially it is a custom query instance.
	///		This is required by Linq. Is not EF specific.
	///		
	/// 2. Remove null-checking wrappers.
	///		This is EF specific issue.
	///		
	/// 3. Remove type conversions.
	///		This is EF specific issue.
	///		
	/// </remarks>
	public class ExpressionFixingVisitor : ExpressionVisitor
	{
		private readonly Type _interceptingQueryType;
		private readonly IQueryable _sourceQuery;

		/// <summary>
		/// Constructs an instance that will fix the root expression node.
		/// </summary>
		/// <param name="interceptingQueryType">Custom query type. This is the type of custom IQueryable implementation.</param>
		/// <param name="sourceQuery">Actual queryable object. E.g. EF data context property value.</param>
		public ExpressionFixingVisitor(Type interceptingQueryType, IQueryable sourceQuery)
		{
			if (interceptingQueryType == null) throw new ArgumentNullException("interceptingQueryType");
			if (sourceQuery == null) throw new ArgumentNullException("sourceQuery");

			_interceptingQueryType = interceptingQueryType;
			_sourceQuery = sourceQuery;
		}

		/// <summary>
		/// Constructs an instance that will not fix the root expression node.
		/// </summary>
		public ExpressionFixingVisitor()
		{
		}

		protected override Expression VisitUnary(UnaryExpression u)
		{
			// Remove type conversions to Object from expression tree since EF LINQ translator does not support it. This is to support OData projections.
			return u != null && u.NodeType == ExpressionType.Convert && u.Type == typeof(object)
				? base.Visit(u.Operand)
				: base.VisitUnary(u);
		}

		protected override Expression VisitConditional(ConditionalExpression c)
		{
			// Remove null-cheking from expression tree since EF LINQ translator does not support it. This is to support OData projections.
			if (c.IfFalse.NodeType == ExpressionType.Constant && ((ConstantExpression) c.IfFalse).Value == null)
			{
				return base.Visit(c.IfTrue);
			}
			if (c.IfTrue.NodeType == ExpressionType.Constant && ((ConstantExpression)c.IfTrue).Value == null)
			{
				return base.Visit(c.IfFalse);
			}
			return base.VisitConditional(c);
		}

		protected override Expression VisitConstant(ConstantExpression c)
		{
			// Replace the root object to which query is applied.
			// Initially the expression tree is built for custom query. So we have to change it to be suitable for data context.
			return c.Value != null && c.Value.GetType() == _interceptingQueryType
				? _sourceQuery.Expression
				: base.VisitConstant(c);
		}
		
		/// <summary>
		/// Changes the query to which the expression tree can be applied.
		/// </summary>
		/// <param name="expression"></param>
		/// <returns></returns>
		public Expression Fix(Expression expression)
		{
			return Visit(expression);
		}
	}
}
